/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2005, University of Maryland
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.ba;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nonnull;

import org.apache.bcel.Constants;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.FieldInstruction;
import org.apache.bcel.generic.InvokeInstruction;
import org.apache.bcel.generic.MethodGen;
import org.objectweb.asm.Opcodes;

import edu.umd.cs.findbugs.FieldAnnotation;
import edu.umd.cs.findbugs.MethodAnnotation;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.classfile.CheckedAnalysisException;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;
import edu.umd.cs.findbugs.classfile.Global;
import edu.umd.cs.findbugs.classfile.IAnalysisCache;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.classfile.analysis.FieldInfo;
import edu.umd.cs.findbugs.classfile.analysis.MethodInfo;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.internalAnnotations.SlashedClassName;
import edu.umd.cs.findbugs.util.ClassName;
import edu.umd.cs.findbugs.visitclass.DismantleBytecode;
import edu.umd.cs.findbugs.visitclass.PreorderVisitor;

/**
 * Factory methods for creating XMethod objects.
 * 
 * @author David Hovemeyer
 */
public class XFactory {
	public static final boolean DEBUG_UNRESOLVED = SystemProperties.getBoolean("findbugs.xfactory.debugunresolved");

	private Set<ClassDescriptor> reflectiveClasses = new HashSet<ClassDescriptor>();
	private Map<MethodDescriptor, XMethod> methods = new HashMap<MethodDescriptor, XMethod>();

	private Map<FieldDescriptor, XField> fields = new HashMap<FieldDescriptor, XField>();

	private Set<XMethod> calledMethods = new HashSet<XMethod>();
	private Set<XField> emptyArrays = new HashSet<XField>();

	private Set<String> calledMethodSignatures = new HashSet<String>();


	public void canonicalizeAll() {
		DescriptorFactory descriptorFactory = DescriptorFactory.instance();
		for(XMethod m : methods.values())
			if (m instanceof MethodDescriptor) {
				descriptorFactory.canonicalize((MethodDescriptor)m);
			}
		for(XField f : fields.values())
			if (f instanceof FieldDescriptor)
				descriptorFactory.canonicalize((FieldDescriptor)f);
	}
	/**
	 * Constructor.
	 */
	public XFactory() {
	}

	public void intern(XClass c) {
		for (XMethod m : c.getXMethods()) {
			MethodInfo mi = (MethodInfo) m;
			methods.put(mi, mi);
		}
		for (XField f : c.getXFields()) {
			FieldInfo fi = (FieldInfo) f;
			fields.put(fi, fi);
		}
	}

	public Collection<XField> allFields() {
		return fields.values();
	}
	public void addCalledMethod(MethodDescriptor m) {
		assert m.getClassDescriptor().getClassName().indexOf('.') == -1;
		calledMethods.add(createXMethod(m));
	}
	public void addEmptyArrayField(XField f) {
		emptyArrays.add(f);
	}
	public boolean isEmptyArrayField(@CheckForNull XField f) {
		return emptyArrays.contains(f);
	}
	public boolean isCalled(XMethod m) {
		if (m.getName().equals("<clinit>"))
			return true;
		return calledMethods.contains(m);
	}

	public Set<XMethod> getCalledMethods() {
		return calledMethods;
	}

	public Set<ClassDescriptor> getReflectiveClasses() {
		return reflectiveClasses;
	}
	public boolean isReflectiveClass(ClassDescriptor c) {
		return reflectiveClasses.contains(c);
	}
	public boolean addReflectiveClasses(ClassDescriptor c) {
		return reflectiveClasses.add(c);
	}
	public boolean isCalledDirectlyOrIndirectly(XMethod m) {
		if (isCalled(m))
			return true;
		if (m.isStatic() || m.isPrivate() || m.getName().equals("<init>"))
			return false;
		try {
			IAnalysisCache analysisCache = Global.getAnalysisCache();
			XClass clazz = analysisCache.getClassAnalysis(XClass.class, m.getClassDescriptor());
			if (isCalledDirectlyOrIndirectly(clazz.getSuperclassDescriptor(), m))
				return true;
			for (ClassDescriptor i : clazz.getInterfaceDescriptorList())
				if (isCalledDirectlyOrIndirectly(i, m))
					return true;

			return false;
		} catch (edu.umd.cs.findbugs.classfile.MissingClassException e) {
			// AnalysisContext.reportMissingClass(e.getClassNotFoundException());
			return false;
		} catch (MissingClassException e) {
			AnalysisContext.reportMissingClass(e.getClassNotFoundException());
			return false;
		} catch (Exception e) {
			AnalysisContext.logError("Error checking to see if " + m + " is called (" + e.getClass().getCanonicalName() + ")", e);
			return false;
		}
	}

	/**
	 * @param superclassDescriptor
	 * @param m
	 * @return
	 * @throws CheckedAnalysisException
	 */
	private boolean isCalledDirectlyOrIndirectly(@CheckForNull
	ClassDescriptor clazzDescriptor, XMethod m) throws CheckedAnalysisException {
		if (clazzDescriptor == null)
			return false;
		IAnalysisCache analysisCache = Global.getAnalysisCache();
		XClass clazz = analysisCache.getClassAnalysis(XClass.class, clazzDescriptor);
		XMethod m2 = clazz.findMethod(m.getName(), m.getSignature(), m.isStatic());
		if (m2 != null && isCalled(m2))
			return true;
		if (isCalledDirectlyOrIndirectly(clazz.getSuperclassDescriptor(), m))
			return true;
		for (ClassDescriptor i : clazz.getInterfaceDescriptorList())
			if (isCalledDirectlyOrIndirectly(i, m))
				return true;

		return false;

	}

	public boolean nameAndSignatureIsCalled(XMethod m) {
		return calledMethodSignatures.contains(getDetailedSignature(m));
	}

	/**
	 * @param m2
	 * @return
	 */
	private static String getDetailedSignature(XMethod m2) {
		return m2.getName() + m2.getSignature() + m2.isStatic();
	}

	@Deprecated
	public boolean isInterned(XMethod m) {
		return m.isResolved();
	}

	public static String canonicalizeString(String s) {
		return DescriptorFactory.canonicalizeString(s);
	}

	/**
	 * Create an XMethod object from a BCEL Method.
	 * 
	 * @param className
	 *            the class to which the Method belongs
	 * @param method
	 *            the Method
	 * @return an XMethod representing the Method
	 */

	public static XMethod createXMethod(String className, Method method) {
		String methodName = method.getName();
		String methodSig = method.getSignature();
		int accessFlags = method.getAccessFlags();

		return createXMethod(className, methodName, methodSig, accessFlags);
	}

	/*
	 * Create a new, never-before-seen, XMethod object and intern it.
	 */
	private static XMethod createXMethod(@DottedClassName
	String className, String methodName, String methodSig, int accessFlags) {
		return createXMethod(className, methodName, methodSig, (accessFlags & Constants.ACC_STATIC) != 0);
	}

	/**
	 * Create an XMethod object from a BCEL Method.
	 * 
	 * @param javaClass
	 *            the class to which the Method belongs
	 * @param method
	 *            the Method
	 * @return an XMethod representing the Method
	 */

	public static XMethod createXMethod(JavaClass javaClass, Method method) {
		if (method == null)
			throw new NullPointerException("method must not be null");
		XMethod xmethod = createXMethod(javaClass.getClassName(), method);
		assert xmethod.isResolved();
		return xmethod;
	}

	public static void assertDottedClassName(@DottedClassName
			String className) {
		assert className.indexOf('/') == -1;
	}
	public static void assertSlashedClassName(@SlashedClassName
			String className) {
		assert className.indexOf('.') == -1;
	}
	
	/**
	 * @param className
	 * @param methodName
	 * @param methodSig
	 * @param isStatic
	 * @return the created XMethod
	 */

	public static XMethod createXMethodUsingSlashedClassName(@SlashedClassName
	String className, String methodName, String methodSig, boolean isStatic) {
		assertSlashedClassName(className);
		MethodDescriptor desc = DescriptorFactory.instance().getMethodDescriptor(className,
		        methodName, methodSig, isStatic);
		return createXMethod(desc);
	}

	/**
	 * @param className
	 * @param methodName
	 * @param methodSig
	 * @param isStatic
	 * @return the created XMethod
	 */

	public static XMethod createXMethod(@DottedClassName
	String className, String methodName, String methodSig, boolean isStatic) {
		assertDottedClassName(className);
		MethodDescriptor desc = DescriptorFactory.instance().getMethodDescriptor(ClassName.toSlashedClassName(className),
		        methodName, methodSig, isStatic);
		return createXMethod(desc);
	}

	public static XMethod createXMethod(MethodDescriptor desc) {
		XFactory xFactory = AnalysisContext.currentXFactory();

		XMethod m = xFactory.methods.get(desc);
		if (m != null)
			return m;
		m = xFactory.resolveXMethod(desc);
		if (m instanceof MethodDescriptor)  {
		  xFactory.methods.put((MethodDescriptor) m, m);
		  DescriptorFactory.instance().canonicalize((MethodDescriptor) m);
		} else 
			xFactory.methods.put(desc, m);
		return m;
	}
	
	public static void profile() {
		XFactory xFactory = AnalysisContext.currentXFactory();
		int count = 0;
		for(XMethod m : xFactory.methods.values()) {
			if (m instanceof MethodInfo)
				count++;
		}
		System.out.printf("XFactory cached methods: %d/%d\n", count, xFactory.methods.size());
		DescriptorFactory.instance().profile();
		
	}

	private XMethod resolveXMethod(MethodDescriptor originalDescriptor) {
		MethodDescriptor desc = originalDescriptor;
		try {
			while (true) {
				XMethod m = methods.get(desc);
				if (m != null)
					return m;
				XClass xClass = Global.getAnalysisCache().getClassAnalysis(XClass.class, desc.getClassDescriptor());
				if (xClass == null)
					break;
				ClassDescriptor superClass = xClass.getSuperclassDescriptor();
				if (superClass == null)
					break;
				desc = DescriptorFactory.instance().getMethodDescriptor(superClass.getClassName(), desc.getName(),
				        desc.getSignature(), desc.isStatic());
			}
		} catch (CheckedAnalysisException e) {
			assert true;
		} catch (RuntimeException e) {
			assert true;
		}
		return new UnresolvedXMethod(originalDescriptor);
	}

	public static XMethod createXMethod(MethodAnnotation ma) {
		return createXMethod(ma.getClassName(), ma.getMethodName(), ma.getMethodSignature(), ma.isStatic());
	}

	/**
	 * Create an XField object
	 * 
	 * @param className
	 * @param fieldName
	 * @param fieldSignature
	 * @param isStatic
	 * @return the created XField
	 */
	public static 
	XField createXFieldUsingSlashedClassName(@SlashedClassName String className, String fieldName, String fieldSignature, boolean isStatic) {
		FieldDescriptor fieldDesc = DescriptorFactory.instance().getFieldDescriptor(className,
				fieldName, fieldSignature, isStatic);
		
		return  createXField(fieldDesc);
	}
	/**
	 * Create an XField object
	 * 
	 * @param className
	 * @param fieldName
	 * @param fieldSignature
	 * @param isStatic
	 * @return the created XField
	 */
	public static 
	XField createXField(@DottedClassName String className, String fieldName, String fieldSignature, boolean isStatic) {
		FieldDescriptor fieldDesc = DescriptorFactory.instance().getFieldDescriptor(ClassName.toSlashedClassName(className),
				fieldName, fieldSignature, isStatic);
		
		return  createXField(fieldDesc);
	}

	public final static boolean DEBUG_CIRCULARITY = SystemProperties.getBoolean("circularity.debug");

	public static XField createXField(FieldInstruction fieldInstruction, ConstantPoolGen cpg) {
		String className = fieldInstruction.getClassName(cpg);
		String fieldName = fieldInstruction.getName(cpg);
		String fieldSig = fieldInstruction.getSignature(cpg);

		int opcode = fieldInstruction.getOpcode();
		return createXField(className, fieldName, fieldSig, opcode == Constants.GETSTATIC || opcode == Constants.PUTSTATIC);
	}

	public static XField createReferencedXField(DismantleBytecode visitor) {
		int seen = visitor.getOpcode();
		if (seen != Opcodes.GETFIELD &&  seen != Opcodes.GETSTATIC && seen != Opcodes.PUTFIELD && seen != Opcodes.PUTSTATIC)
			throw new IllegalArgumentException("Not at a field reference");
		return createXFieldUsingSlashedClassName(visitor.getClassConstantOperand(), visitor.getNameConstantOperand(), visitor
		        .getSigConstantOperand(), visitor.getRefFieldIsStatic());
	}

	public static XMethod createReferencedXMethod(DismantleBytecode visitor) {
		return createXMethodUsingSlashedClassName(visitor.getClassConstantOperand(), visitor.getNameConstantOperand(), visitor
		        .getSigConstantOperand(), visitor.getOpcode() == Constants.INVOKESTATIC);
	}

	public static XField createXField(FieldAnnotation f) {
		return createXField(f.getClassName(), f.getFieldName(), f.getFieldSignature(), f.isStatic());
	}

	public static XField createXField(JavaClass javaClass, Field field) {
		return createXField(javaClass.getClassName(), field);
	}

	/**
	 * Create an XField object from a BCEL Field.
	 * 
	 * @param className
	 *            the name of the Java class containing the field
	 * @param field
	 *            the Field within the JavaClass
	 * @return the created XField
	 */
	public static XField createXField(String className, Field field) {
		String fieldName = field.getName();
		String fieldSig = field.getSignature();

		XField xfield = getExactXField(className, fieldName, fieldSig, field.isStatic());
		assert xfield.isResolved() : "Could not exactly resolve " + xfield;
		return xfield;
	}

	/**
	 * Get an XField object exactly matching given class, name, and signature.
	 * May return an unresolved object (if the class can't be found, or does not
	 * directly declare named field).
	 * 
	 * @param className
	 *            name of class containing the field
	 * @param name
	 *            name of field
	 * @param signature
	 *            field signature
	 * @param isStatic
	 *            field access flags
	 * @return XField exactly matching class name, field name, and field
	 *         signature
	 */
	public static XField getExactXField(@SlashedClassName String className, String name, String signature, boolean isStatic) {
		FieldDescriptor fieldDesc = DescriptorFactory.instance().getFieldDescriptor(ClassName.toSlashedClassName(className),
		        name, signature, isStatic);
		return getExactXField(fieldDesc);
	}

	public static @Nonnull XField getExactXField(@SlashedClassName String className, Field f) {
		FieldDescriptor fd = DescriptorFactory.instance().getFieldDescriptor(className, f);
		return getExactXField(fd);
	}
	public static @Nonnull XField getExactXField(FieldDescriptor desc) {
		XFactory xFactory = AnalysisContext.currentXFactory();

		XField f = xFactory.fields.get(desc);
		if (f == null) return new UnresolvedXField(desc);
		return f;
	}
	public static XField createXField(FieldDescriptor desc) {
		XFactory xFactory = AnalysisContext.currentXFactory();

		XField m = xFactory.fields.get(desc);
		if (m != null)
			return m;
		m = xFactory.resolveXField(desc);
		xFactory.fields.put(desc, m);
		return m;
	}
	private XField resolveXField(FieldDescriptor originalDescriptor) {
		FieldDescriptor desc = originalDescriptor;
		LinkedList<ClassDescriptor> worklist = new LinkedList<ClassDescriptor>();
		ClassDescriptor originalClassDescriptor = desc.getClassDescriptor();
		worklist.add(originalClassDescriptor);
		try {
			while (!worklist.isEmpty()) {
				ClassDescriptor d = worklist.removeFirst();
				if (!d.equals(originalClassDescriptor)) 
					desc = DescriptorFactory.instance().getFieldDescriptor(d.getClassName(), desc.getName(),
					        desc.getSignature(), desc.isStatic());
				
				XField f = fields.get(desc);
				if (f != null) 
					return f;
				XClass xClass = Global.getAnalysisCache().getClassAnalysis(XClass.class, d);
				if (xClass == null)
					break;
				ClassDescriptor superClass = xClass.getSuperclassDescriptor();
				if (superClass != null)
					worklist.add(superClass);
				if (originalDescriptor.isStatic())
				  for(ClassDescriptor i : xClass.getInterfaceDescriptorList()) 
					worklist.add(i);

			}
		} catch (CheckedAnalysisException e) {
			AnalysisContext.logError("Error resolving " + originalDescriptor, e);
		}
		return new UnresolvedXField(originalDescriptor);
	}

	/**
	 * Create an XMethod object from an InvokeInstruction.
	 * 
	 * @param invokeInstruction
	 *            the InvokeInstruction
	 * @param cpg
	 *            ConstantPoolGen from the class containing the instruction
	 * @return XMethod representing the method called by the InvokeInstruction
	 */
	public static XMethod createXMethod(InvokeInstruction invokeInstruction, ConstantPoolGen cpg) {
		String className = invokeInstruction.getClassName(cpg);
		String methodName = invokeInstruction.getName(cpg);
		String methodSig = invokeInstruction.getSignature(cpg);

		return createXMethod(className, methodName, methodSig, invokeInstruction.getOpcode() == Constants.INVOKESTATIC);
	}

	/**
	 * Create an XMethod object from the method currently being visited by the
	 * given PreorderVisitor.
	 * 
	 * @param visitor
	 *            the PreorderVisitor
	 * @return the XMethod representing the method currently being visited
	 */
	public static XMethod createXMethod(PreorderVisitor visitor) {
		JavaClass javaClass = visitor.getThisClass();
		Method method = visitor.getMethod();
		XMethod m = createXMethod(javaClass, method);
		return m;
	}

	/**
	 * Create an XField object from the field currently being visited by the
	 * given PreorderVisitor.
	 * 
	 * @param visitor
	 *            the PreorderVisitor
	 * @return the XField representing the method currently being visited
	 */
	public static XField createXField(PreorderVisitor visitor) {
		JavaClass javaClass = visitor.getThisClass();
		Field field = visitor.getField();
		XField f = createXField(javaClass, field);
		return f;
	}

	public static XMethod createXMethod(MethodGen methodGen) {
		String className = methodGen.getClassName();
		String methodName = methodGen.getName();
		String methodSig = methodGen.getSignature();
		int accessFlags = methodGen.getAccessFlags();
		return createXMethod(className, methodName, methodSig, accessFlags);
	}

	public static XMethod createXMethod(JavaClassAndMethod classAndMethod) {
		return createXMethod(classAndMethod.getJavaClass(), classAndMethod.getMethod());
	}

	/**
	 * Get the XClass object providing information about the class named by the
	 * given ClassDescriptor.
	 * 
	 * @param classDescriptor
	 *            a ClassDescriptor
	 * @return an XClass object providing information about the class, or null
	 *         if the class cannot be found
	 */
	public @CheckForNull XClass getXClass(ClassDescriptor classDescriptor) {
		try {
			IAnalysisCache analysisCache = Global.getAnalysisCache();
			return analysisCache.getClassAnalysis(XClass.class, classDescriptor);
		} catch (CheckedAnalysisException e) {
			return null;
		}
	}

	/**
	 * Compare XMethod or XField object objects.
	 * <em>All methods that implement XMethod or XField should
	 * delegate to this method when implementing compareTo(Object)
	 * if the right-hand object implements XField or XMethod.</em>
	 * 
	 * @param lhs
	 *            an XMethod or XField
	 * @param rhs
	 *            an XMethod or XField
	 * @return comparison of lhs and rhs
	 */
	public static <E extends ClassMember> int compare(E lhs, E rhs) {
		int cmp;

		cmp = lhs.getClassName().compareTo(rhs.getClassName());
		if (cmp != 0) {
			return cmp;
		}

		cmp = lhs.getName().compareTo(rhs.getName());
		if (cmp != 0) {
			return cmp;
		}

		cmp = lhs.getSignature().compareTo(rhs.getSignature());
		if (cmp != 0) {
			return cmp;
		}

		return (lhs.isStatic() ? 1 : 0) - (rhs.isStatic() ? 1 : 0);
	}

}
